
#include "stdafx.h"

#include "JSAPI.h"
#include "LPCSTRArray.h"
#include "WebkitView.h"
#include "LogClass.h"
#include <map>
using namespace std;

CString CJSAPI::JSValueToString(JSContextRef ctx, JSValueRef value)
{
	CString msg; JSStringRef str = nullptr;
	do {
		JSValueRef exp = nullptr; str = JSValueToStringCopy(ctx, value, &exp); if (!str || exp) break;
		const JSChar* p = JSStringGetCharactersPtr(str); if (!p) break;
		msg = CString(p, (int)JSStringGetLength(str));
	} while (0);
	if (str) JSStringRelease(str);
	return msg;
}

bool CJSAPI::checkException(JSValueRef exception, JSValueRef retValue, CString &msg) const
{
	if (exception) { msg.Format(L"exception %s", JSValueToString(exception)); return true; }
	if (!retValue) { msg = L"return null"; return true; }
	return false;
}

JSValueRef CJSAPI::evaluateScript(LPCSTR script, JSObjectRef thisObject)
{
	JSValueRef exception = nullptr;
	JSValueRef result = JSEvaluateScript(m_context, APIString(script), thisObject, nullptr, 0, &exception);
	if (exception) { errmsg.Format(L"evaluateScript: exception %s", JSValueToString(exception)); return nullptr; }
	return result;
}

JSObjectRef CJSAPI::newInstance(LPCSTR className, size_t argc, const JSValueRef arguments[])
{
	JSObjectRef globalObj = nullptr; if (!checkNullObj(globalObj)) return nullptr;
	JSValueRef exception = nullptr; CString msg;
	JSValueRef value = JSObjectGetProperty(m_context, globalObj, APIString(className), &exception);
	if (checkException(exception, value, msg)) { errmsg.Format(L"newInstance: get class %s", msg); return nullptr; }
	JSObjectRef obj = JSValueToObject(m_context, value, &exception);
	if (checkException(exception, obj, msg)) { errmsg.Format(L"newInstance: none class %s", msg); return nullptr; }
	if (!JSObjectIsConstructor(m_context, obj)) { errmsg = L"newInstance: none construct"; return nullptr; }
	JSObjectRef result = JSObjectCallAsConstructor(m_context, obj, argc, arguments, &exception);
	if (checkException(exception, result, msg)) { errmsg.Format(L"newInstance: call construct %s", msg); return nullptr; }
	return result;
}

JSObjectRef CJSAPI::newFunction(LPCSTR funcName, JSObjectCallAsFunctionCallback funcBody, JSObjectRef thisObject)
{
	if (!checkNullObj(thisObject)) return nullptr;
	JSObjectRef func = JSObjectMakeFunctionWithCallback(m_context, APIString(funcName), funcBody);
	if (!func) { errmsg = L"newFunction: make function"; return nullptr; }
	if (!funcName || setProperty(funcName, func, kJSPropertyAttributeNone, thisObject)) return func;
	return nullptr;
}

JSValueRef CJSAPI::getProperty(LPCSTR propNames, JSObjectRef thisObject)
{
	if (!checkNullObj(thisObject)) return nullptr;
	CLPCSTRArray ary(propNames, '.'); JSValueRef result = nullptr, exception = nullptr; CString msg;
	for (intptr_t i = 0; i < ary.count; i++)
	{
		result = JSObjectGetProperty(m_context, thisObject, APIString(ary.str[i]), &exception);
		if (checkException(exception, result, msg)) { errmsg.Format(L"getProperty: %S %s", ary.str[i], msg); return nullptr; }
		if (i < ary.count - 1)
		{
			thisObject = JSValueToObject(m_context, result, &exception);
			if (checkException(exception, thisObject, msg)) { errmsg.Format(L"getProperty: %S %s", ary.str[i], msg); return nullptr; }
		}
	}
	return result;
}

bool CJSAPI::setProperty(LPCSTR propNames, JSValueRef value, JSPropertyAttributes attributes, JSObjectRef thisObject)
{
	if (!checkNullObj(thisObject)) return false;
	CLPCSTRArray ary(propNames, '.'); JSValueRef exception = nullptr; CString msg;
	for (intptr_t i = 0; i < ary.count; i++)
	{
		if (i < ary.count - 1)
		{
			JSValueRef result = JSObjectGetProperty(m_context, thisObject, APIString(ary.str[i]), &exception);
			if (checkException(exception, result, msg)) { errmsg.Format(L"setProperty: %S %s", ary.str[i], msg); return false; }
			thisObject = JSValueToObject(m_context, result, &exception);
			if (checkException(exception, thisObject, msg)) { errmsg.Format(L"setProperty: %S %s", ary.str[i], msg); return false; }
		}
		else if (value)
		{
			JSObjectSetProperty(m_context, thisObject, APIString(ary.str[i]), value, attributes, &exception);
			if (exception) { errmsg.Format(L"setProperty failed exception %s", JSValueToString(exception)); return false; }
			return true;
		}
		else
		{
			bool delOK = JSObjectDeleteProperty(m_context, thisObject, APIString(ary.str[i]), &exception);
			if (exception) { errmsg.Format(L"delProperty exception %s", JSValueToString(exception)); return false; }
			else if (!delOK) { errmsg = L"delProperty failed"; return false; }
			return true;
		}
	}
	errmsg = L"setProperty: null name";	return false;
}

JSObjectRef CJSAPI::getFunction(LPCSTR funcName, JSObjectRef thisObject)
{
	JSValueRef value = getProperty(funcName, thisObject);
	if (!value) { errmsg.Format(L"getFunction: [%S] not found", funcName); return nullptr; }
	JSValueRef exception = nullptr; CString msg;
	JSObjectRef func = JSValueToObject(m_context, value, &exception);
	if (checkException(exception, func, msg)) { errmsg.Format(L"getFunction: [%S] not function %s", funcName, msg); return nullptr; }
	return JSObjectIsFunction(m_context, func) ? func : nullptr;
}

//__declspec(noinline)
JSValueRef CJSAPI::callFunction(JSObjectRef func, size_t argc, const JSValueRef arguments[], JSObjectRef thisObject, JSValueRef* exception)
{
	if (!func || !JSObjectIsFunction(m_context,func)) { errmsg = L"callFunction: null function"; return nullptr; }
	JSValueRef exp = nullptr; CString msg; if (!exception) exception = &exp;
	JSValueRef result = JSObjectCallAsFunction(m_context, func, thisObject, argc, arguments, exception);
	if (checkException(*exception, result, msg)) { errmsg.Format(L"callFunction: %s", msg); return nullptr; }
	return result;
}

bool CJSAPI::addActiveX(bool add)
{
	if (add)
		return setProperty("ActiveXObject", JSValueMakeBoolean(m_context, false), kJSPropertyAttributeReadOnly);
	else
		return setProperty("ActiveXObject", nullptr, 0);
}

bool CJSAPI::checkNullObj(JSObjectRef &thisObject)
{
	if (!thisObject)
	{
		thisObject = JSContextGetGlobalObject(m_context);
		if (!thisObject) errmsg = L"checkNullObj: null GlobalObject";
	}
	return !!thisObject;
}

JSObjectRef CJSAPI::SetHook(LPCSTR funcName, JSObjectCallAsFunctionCallback funcHook, JSObjectRef thisObject)
{
	if (!checkNullObj(thisObject)) return false;
	JSObjectRef funcObj = getFunction(funcName, thisObject); if (!funcObj) return false;
	char funcHookName[1024]; sprintf_s(funcHookName, "%s_hook", funcName);
	JSObjectRef hookFunc = newFunction(funcName, funcHook, thisObject); if (!hookFunc) return false;
	if (setProperty(funcHookName, funcObj, kJSPropertyAttributeNone, thisObject)) return funcObj;
	return nullptr;
}

#ifdef _WEBKIT_NEW

static vmpi_spin_lock mutationMapLock;
static map<JSObjectRef, CWebkitView*> mutationMap;

auto MutationCallback = [](JSContextRef ctx, JSObjectRef function, JSObjectRef thisObject,
	size_t argumentCount, const JSValueRef arguments[], JSValueRef *exception) -> JSValueRef {
	ASSERT_MainThread();
	mutationMapLock.Lock();
	auto iter = mutationMap.find(thisObject);
	CWebkitView *view = iter != mutationMap.end() ? iter->second : nullptr;
	mutationMapLock.Unlock();
	if (view) view->CheckWindownType();
	return JSValueMakeUndefined(ctx);
};

bool CJSAPI::installMutationObserver(CWebkitView *caller)
{
	JSValueRef args[2]; if ((args[0] = newFunction(nullptr, MutationCallback)) == nullptr)
	{
		errmsg.Insert(0, L"MutationCallback failed: "); return false;
	}
	JSObjectRef mutation = newInstance("MutationObserver", 1, args); if (!mutation)
	{
		errmsg.Insert(0, L"new MutationObserver failed: "); return false;
	}
	args[0] = getProperty("document.body"); if (!args[0])
	{
		errmsg.Insert(0, L"get body failed: "); return false;
	}
	args[1] = evaluateScript("({ childList:true, subtree:true })"); if (!args[1])
	{
		errmsg.Insert(0, L"init option failed: "); return false;
	}
	if (callFunction("observe", 2, args, mutation) == nullptr)
	{
		errmsg.Insert(0, L"call observe failed: "); return false;
	}
	mutationMapLock.Lock();
	for (auto iter = mutationMap.begin(); iter != mutationMap.end(); iter++) if (iter->second == caller)
	{
		mutationMap.erase(iter); break;
	}
	mutationMap[mutation] = caller;
	mutationMapLock.Unlock();
	return true;
}

bool CJSAPI::fastLogin(LPCSTR script)
{
	JSValueRef ret = evaluateScript(script);
	if (!ret)
	{
		errmsg.Insert(0, L"fastLogin failed: "); return false;
	}
	return JSValueToBoolean(m_context, ret);
}

bool CJSAPI::GetWinSize(CSize &winSize)
{
	JSValueRef value = getProperty("screen"); if (!value) return false;
	JSValueRef exception = nullptr;
	JSObjectRef screen = JSValueToObject(m_context, value, &exception);
	if (exception || !screen) return false;
	int width, height;
	if (value = getProperty("availWidth", screen))
	{
		width = (int)JSValueToNumber(m_context, value, &exception); if (exception) return false;
	}
	if (value = getProperty("availHeight", screen))
	{
		height = (int)JSValueToNumber(m_context, value, &exception); if (exception) return false;
	}
	winSize.cx = width; winSize.cy = height;
	return true;
}

bool CJSAPI::GetRotateRate(LPCWSTR DragWrapName, LPCWSTR DragBtnName, double &rate)
{
	char v[1024]; sprintf_s(v, "(Math.round(360 / (%S.width() - %S.outerWidth()) * 100) / 100)", DragWrapName, DragBtnName);
	JSValueRef value = evaluateScript(v);
	if (!value)
	{
		errmsg.Insert(0, L"GetRotateRate failed: "); return false;
	}
	JSValueRef exception = nullptr;
	rate = JSValueToNumber(m_context, value, &exception);
	return !exception;
}

#endif	//_WEBKIT_NEW
